package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.RedisData;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisUtil;
import com.hmdp.utils.SystemConstants;
import javafx.util.converter.LocalDateTimeStringConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedisUtil redisUtil;
    /* private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(15);

    @Override
    public Shop ShopCache(Long id) {
        //todo:1.通过id组成key，去redis中查询是否有该id对应的shop的缓存
        String key = "shop_cache:" + String.valueOf(id);
        String shop = stringRedisTemplate.opsForValue().get(key);

        //todo:2.判断这个返回结果是否为空
        if (StrUtil.isBlank(shop)) {
            //如果为空，那么则表示缓存未命中，我们就要到数据库中去查询数据返回结果并将未命中的数据更新到redis中去
            //todo:3.1从数据库中获取数据
            Shop byId = getById(id);

            //todo:修改解决缓存穿透问题
            if (byId == null) {
                //在redis中不存在，在数据库中也不存在则把空对象存入redis中，并设置TTL
                Shop shop1 = new Shop();
                String emptyShop = JSONUtil.toJsonStr(shop1);
                //这里抛异常了，看来是不能存null
                stringRedisTemplate.opsForValue().set(key, emptyShop, 60, TimeUnit.SECONDS);
                return byId;
            }
            //todo:3.2将数据存入redis中充当缓存
            String shopJsonString = JSONUtil.toJsonStr(byId);
            //修改：给缓存信息设置了ttl作为缓存更新的兜底策略
            stringRedisTemplate.opsForValue().set(key, shopJsonString, 10, TimeUnit.MINUTES);
            return byId;
        }
        //todo:shopMap不为空的话，表示在redis中缓存命中，那么直接返回将返回的json字符串转换为shop返回
        Shop shop1 = JSONUtil.toBean(shop, Shop.class);
        if (shop1 == null) {
            return shop1;
        }
        return shop1;
    }

    @Override
    @Transactional
    public Boolean shopUpdateCache(Shop shop) {
        //todo:我们的主动更新策略是----->先修改数据库，再去删除redis中对应的缓存
        //1.修改数据库数据
        boolean updateDataBase = this.updateById(shop);
        //2.去删除掉redis中对应的缓存
        Long id = shop.getId();
        String key = "shop_cache:" + String.valueOf(id);
        Boolean deleteRedisCache = stringRedisTemplate.delete(key);
        return true;
    }

    @Override
    public Shop queryWithMutexLock(Long id) {
        //解决缓存击穿的
        String key = "shop_cache:" + String.valueOf(id);
        String shop = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(shop)) {
            //redis中未命中，则尝试去获取互斥锁
            String lockKey = "shop_lock:" + String.valueOf(id);
            Shop byId = null;
            try {
                boolean lock = getLock(lockKey);
                if (lock == false) {
                    //获取互斥锁失败，表明前面已有线程获取互斥锁成功，我们只需等待前面线程完成缓存更新=
                    Thread.sleep(50);
                    return queryWithMutexLock(id);
                }
                //获取互斥锁成功，根据id去数据库查询数据
                byId = getById(id);
                if (byId == null) {
                    return byId;
                }
                String shopJsonString = JSONUtil.toJsonStr(byId);
                stringRedisTemplate.opsForValue().set(key, shopJsonString);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                //缓存写入完成后，便会释放互斥锁
                unLock(lockKey);
            }
            return byId;
        }
        //redis中命中，则将数据转换为shop，返回
        Shop shop1 = JSONUtil.toBean(shop, Shop.class);
        return shop1;
    }

    @Override
    public Shop quertWithLogic(Long id) {
        //获取到前端传来的id
        String key = "shop_cache:" + String.valueOf(id);
        //通过封装好的key，去redis中查询缓存
        String shop = stringRedisTemplate.opsForValue().get(key);
        //未命中，直接返回null
        if (StrUtil.isBlank(shop)) {
            return null;
        }
        //下面的逻辑则是在redis命中了，判断是否逻辑过期
        RedisData redisData = JSONUtil.toBean(shop, RedisData.class);
        //获取时间来判断是否过期
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isBefore(LocalDateTime.now())) {
            //如果这个缓存的expireTime在当前时间之前，则表示这个缓存已经过期，尝试去获取锁
            String lockKey = "shop_lock:" + String.valueOf(id);
            if (getLock(lockKey)){
                //获取锁成功，则开启新的线程去完成缓存写入
                CACHE_REBUILD_EXECUTOR.submit(() ->{
                   //查询数据库，进行缓存的重建
                    try {
                        saveShopToRedisWithLogic(id,20L);
                        Thread.sleep(50);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        unLock(lockKey);
                    }
                });
            }
            //获取锁不成功，直接返回过期信息
            JSONObject shop1 = (JSONObject) redisData.getData();
            Shop shop2 = shop1.toBean(Shop.class);
            return shop2;
        }
        //没有过期，则直接返回当前redis中的这个缓存
        JSONObject shop3 = (JSONObject) redisData.getData();
        Shop shop4 = shop3.toBean(Shop.class);
        return shop4;

    }


    //获取互斥锁
    private boolean getLock(String lockKey) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "互斥所已开启", 8, TimeUnit.SECONDS);
        boolean aTrue = BooleanUtil.isTrue(flag);
        return aTrue;
    }

    //释放互斥锁
    private boolean unLock(String lockKey) {
        Boolean delete = stringRedisTemplate.delete(lockKey);
        boolean aTrue = BooleanUtil.isTrue(delete);
        return aTrue;
    }

    //将数据库中查到的Shop对象写入redis中充当缓存的方法
    public void saveShopToRedis(Long id,Long ttl){
        Shop byId = getById(id);
        String shop = JSONUtil.toJsonStr(byId);
        String key = "shop_cache:" + String.valueOf(id);
        stringRedisTemplate.opsForValue().set(key,shop,ttl,TimeUnit.SECONDS);
    }

    //将数据库中查到的Shop对象写入redis中充当缓存的方法（用在逻辑过期中的）
    public void saveShopToRedisWithLogic(Long id,Long expireSeconds){
        Shop byId = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(byId);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        String shop = JSONUtil.toJsonStr(redisData);
        //存入redis充当缓存
        String key = "shop_cache:" + String.valueOf(id);
        stringRedisTemplate.opsForValue().set(key,shop);
    }*/

    @Override
    @Transactional
    public Boolean shopUpdateCache(Shop shop) {
        //todo:我们的主动更新策略是----->先修改数据库，再去删除redis中对应的缓存
        //1.修改数据库数据
        boolean updateDataBase = this.updateById(shop);
        //2.去删除掉redis中对应的缓存
        Long id = shop.getId();
        String key = "shop_cache:" + String.valueOf(id);
        Boolean deleteRedisCache = stringRedisTemplate.delete(key);
        return true;
    }

    @Override
    public Result queryById(Long id) {
        //解决缓存穿透问题------>测试成功!
        /*Shop shop = redisUtil.queryWithPassThrough
                ("through_cache_", id, Shop.class, 60L, TimeUnit.SECONDS, this::getById);*/

        //解决缓存击穿问题（互斥锁）----->测试
        /*Shop shop = redisUtil.qureyWithMutex
                ("mutex_shop_", id, Shop.class, 60L, TimeUnit.SECONDS, this::getById);*/

        //解决缓存击穿问题（逻辑过期）----->测试成功！
        Shop shop = redisUtil.queryWithLogicalExpire(
                "logical_shop_", id, Shop.class, 60L, TimeUnit.SECONDS, this::getById);

        if (shop == null) {
            return Result.fail("店铺不存在！");
        }
        return Result.ok(shop);
    }

    //实现附件商店功能记得要换成高版本的redis，不然没有geosearch（）的aip
    @Override
    public Result nearShop(Integer typeId, Integer current, Double x, Double y) {
        //todo:在这结合redis中的geo数据，实现距离最近排序，同时实现分页功能
        //1.判断是否需要按坐标距离查询
        if (x == null || y == null) {
            // 不是按照距离查询
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page.getRecords());
        }

        //2.是按照距离查询，则先计算好分页参数，因为不是对mysql数据库查询，就没有封装好的方法，所以我们要自己计算分页参数
        //current 为当前页码
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE; //从第几个开始
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE; //到第几个结束


        //3.查询redis中的geo数据，下面演示圆心查找周边
        // geosearch key bylonlat x(经度) y（纬度） byradius 半径(默认m)
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .search("geo:" + typeId.toString(),
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
                );
        if (results == null) {
            return Result.ok();
        }
        //这里这个list中，距离前端传来的坐标为圆心，5000m内距离按小到大排序的shopId的集合，分页是从第一个到我们指定的end，
        //因为没办法指定分页的开始位置，所以我们在这里需要自行截取
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();

        if (list.size()<=from){
            //没有更多结果了
            return Result.ok();
        }
        //使用stream流的方式，跳过from之前的shopId实现截取从 from到end的分页
        List<Long> shopIds = new ArrayList<>(list.stream().skip(from).collect(Collectors.toList()).size());
        Map<String, Distance> distanceMap = new HashMap<>(list.size());
        list.stream().skip(from).forEach(result -> {
            String shopId = result.getContent().getName();
            shopIds.add(Long.valueOf(shopId));
            Distance distance = result.getDistance();
            distanceMap.put(shopId, distance);
        });

        //根据shopid的list去数据库查询，因为要按顺序得到返回结果，所有用query().in()方法
        String idStr = StrUtil.join(",", shopIds);
        //fixme:sql出错了
        List<Shop> shops = query().in("id", shopIds).last("order by field(id," + idStr + ")").list();

        //将距离信息distance set到每一个对应的shop中
        for (Shop shop : shops) {
            String shopId = shop.getId().toString();
            Distance distance = distanceMap.get(shopId);
            shop.setDistance(distance.getValue());
        }

        return Result.ok(shops);
    }


}
